﻿using Microsoft.VisualStudio.TestTools.UnitTesting;

using System;

namespace NoSeam.AudioTools.CueSheet.Tests
{
	[TestClass]
	public class TimecodeTests
	{
		[TestClass]
		public class DefaultConstructor : TimecodeTests
		{
			[TestMethod]
			public void SetsOffsetToZero()
			{
				Timecode timecode = new Timecode();
				Assert.AreEqual(timecode.Offset, 0);
			}
		}

		[TestClass]
		public class ConstructorFromOffset : TimecodeTests
		{
			[TestMethod]
			public void AcceptsZero()
			{
				Timecode timecode = new Timecode(0);
				Assert.AreEqual(timecode.Offset, 0);
			}

			[TestMethod]
			public void AcceptsOne()
			{
				Timecode timecode = new Timecode(1);
				Assert.AreEqual(timecode.Offset, 1);
			}

			[TestMethod]
			public void AcceptsMaxInt()
			{
				Timecode timecode = new Timecode(int.MaxValue);
				Assert.AreEqual(timecode.Offset, int.MaxValue);
			}

			[TestMethod]
			[ExpectedException(typeof(ArgumentOutOfRangeException))]
			public void DoesNotAcceptNegativeValue()
			{
				Timecode timecode = new Timecode(-1);
			}
		}

		[TestClass]
		public class ConstructorFromIndividualParts : TimecodeTests
		{
			[TestMethod]
			public void CanCreateZeroOffset()
			{
				Timecode timecode = new Timecode(0, 0, 0);
				Assert.AreEqual(timecode.Offset, 0);
			}

			[TestMethod]
			public void CanCreateSmallOffset()
			{
				Timecode timecode = new Timecode(0, 0, 1);
				Assert.AreEqual(timecode.Offset, 1);
			}

			[TestMethod]
			public void CanCreateLargestOffset()
			{
				Timecode timecode = new Timecode(0, 1, int.MaxValue - Timecode.FramesPerSecond);
				Assert.AreEqual(timecode.Offset, int.MaxValue);
			}

			[TestMethod]
			[ExpectedException(typeof(ArgumentOutOfRangeException))]
			public void CannotCreateTooLargeOffset()
			{
				Timecode timecode = new Timecode(0, 1, int.MaxValue);
			}

			[TestMethod]
			public void AcceptsNegativeMinutes()
			{
				Timecode timecode = new Timecode(-1, Timecode.SecondsPerMinute, 0);
				Assert.AreEqual(timecode.Offset, 0);
			}

			[TestMethod]
			public void AcceptsNegativeSeconds()
			{
				Timecode timecode = new Timecode(0, -1, Timecode.FramesPerSecond);
				Assert.AreEqual(timecode.Offset, 0);
			}

			[TestMethod]
			public void AcceptsNegativeFrames()
			{
				Timecode timecode = new Timecode(0, 1, -Timecode.FramesPerSecond);
				Assert.AreEqual(timecode.Offset, 0);
			}

			[TestMethod]
			[ExpectedException(typeof(ArgumentOutOfRangeException))]
			public void CannotCreateNegativeOffset()
			{
				Timecode timecode = new Timecode(0, 0, -1);
			}
		}

		[TestClass]
		public class ParseMethod : TimecodeTests
		{
			[TestMethod]
			[ExpectedException(typeof(ArgumentNullException))]
			public void CannotParseNull()
			{
				Timecode timecode = Timecode.Parse(null);
			}

			[TestMethod]
			[ExpectedException(typeof(ArgumentException))]
			public void RequiresAllThreeParts()
			{
				Timecode timecode = Timecode.Parse("0:0");
				Assert.AreEqual(timecode.Offset, 0);
			}

			[TestMethod]
			[ExpectedException(typeof(ArgumentException))]
			public void RequiresDigitsOnlyParts()
			{
				Timecode timecode = Timecode.Parse("+1:0:0");
				Assert.AreEqual(timecode.Offset, 0);
			}

			[TestMethod]
			[ExpectedException(typeof(ArgumentException))]
			public void DoesNotAcceptTrailingEndOfLineCharacter()
			{
				Timecode timecode = Timecode.Parse("0:0:0\n");
				Assert.AreEqual(timecode.Offset, 0);
			}

			[TestMethod]
			[ExpectedException(typeof(OverflowException))]
			public void DoesNotAcceptVeryLargeIntegers()
			{
				Timecode timecode = Timecode.Parse(string.Format("0:0:1{0}", int.MaxValue));
				Assert.AreEqual(timecode.Offset, 0);
			}

			[TestMethod]
			public void AcceptsIntegersWithMoreThanTwoDigits()
			{
				Timecode timecode = Timecode.Parse("0:100:0");
				Assert.AreEqual(timecode.Offset, 100 * Timecode.FramesPerSecond);
			}

			[TestMethod]
			public void CanCreateZeroOffset()
			{
				Timecode timecode = Timecode.Parse("00:00:00");
				Assert.AreEqual(timecode.Offset, 0);
			}

			[TestMethod]
			public void CanCreateSmallOffset()
			{
				Timecode timecode = Timecode.Parse("0:0:1");
				Assert.AreEqual(timecode.Offset, 1);
			}

			[TestMethod]
			public void CanCreateLargestOffset()
			{
				Timecode timecode = Timecode.Parse(string.Format("0:1:{0}", int.MaxValue - Timecode.FramesPerSecond));
				Assert.AreEqual(timecode.Offset, int.MaxValue);
			}

			[TestMethod]
			[ExpectedException(typeof(ArgumentOutOfRangeException))]
			public void CannotCreateTooLargeOffset()
			{
				Timecode timecode = Timecode.Parse(string.Format("0:1:{0}", int.MaxValue));
			}
		}

		[TestClass]
		public class ZeroField : TimecodeTests
		{
			[TestMethod]
			public void HasZeroOffset()
			{
				Assert.AreEqual(Timecode.Zero.Offset, 0);
			}

			[TestMethod]
			public void HasZeroMinutes()
			{
				Assert.AreEqual(Timecode.Zero.Minutes, 0);
			}

			[TestMethod]
			public void HasZeroSeconds()
			{
				Assert.AreEqual(Timecode.Zero.Seconds, 0);
			}

			[TestMethod]
			public void HasZeroFrames()
			{
				Assert.AreEqual(Timecode.Zero.Frames, 0);
			}
		}

		[TestClass]
		public class OffsetProperty : TimecodeTests
		{
			[TestMethod]
			[ExpectedException(typeof(ArgumentOutOfRangeException))]
			public void CannotBeSetToNegativeValue()
			{
				Timecode timecode = new Timecode();
				timecode.Offset = -1;
			}

			[TestMethod]
			public void CanBeSetToOne()
			{
				Timecode timecode = new Timecode();
				timecode.Offset = 1;
				Assert.AreEqual(timecode.Offset, 1);
			}

			[TestMethod]
			public void CanBeSetToMaxInt()
			{
				Timecode timecode = new Timecode();
				timecode.Offset = int.MaxValue;
				Assert.AreEqual(timecode.Offset, int.MaxValue);
			}

			[TestMethod]
			public void CanBeAddedTo()
			{
				Timecode timecode = new Timecode();
				timecode.Offset += int.MaxValue;
				Assert.AreEqual(timecode.Offset, int.MaxValue);
			}

			[TestMethod]
			public void CanAddNegativeValue()
			{
				Timecode timecode = new Timecode(int.MaxValue);
				timecode.Offset += -int.MaxValue;
				Assert.AreEqual(timecode.Offset, 0);
			}

			[TestMethod]
			[ExpectedException(typeof(ArgumentOutOfRangeException))]
			public void CanNotOverflow()
			{
				Timecode timecode = new Timecode(int.MaxValue);
				++timecode.Offset;
			}

			[TestMethod]
			public void CanBeSubtractedFrom()
			{
				Timecode timecode = new Timecode(10);
				timecode.Offset -= 9;
				Assert.AreEqual(timecode.Offset, 1);
			}

			[TestMethod]
			public void CanSubtractNegativeValue()
			{
				Timecode timecode = Timecode.Zero;
				timecode.Offset -= -int.MaxValue;
				Assert.AreEqual(timecode.Offset, int.MaxValue);
			}

			[TestMethod]
			[ExpectedException(typeof(ArgumentOutOfRangeException))]
			public void CanNotUnderflow()
			{
				Timecode timecode = Timecode.Zero;
				--timecode.Offset;
			}
		}

		[TestClass]
		public class MinutesProperty : TimecodeTests
		{
			[TestMethod]
			public void CanExceed59()
			{
				Timecode timecode = new Timecode(int.MaxValue);
				Assert.IsTrue(timecode.Minutes > 59);
			}

			[TestMethod]
			public void IsStable()
			{
				Timecode timecode = new Timecode(1, 2, 3);
				Assert.AreEqual(timecode.Minutes, 1);
			}
		}

		[TestClass]
		public class SecondsProperty : TimecodeTests
		{
			[TestMethod]
			public void CarriesOver()
			{
				Timecode timecode = new Timecode(0, Timecode.SecondsPerMinute, 0);
				Assert.AreEqual(timecode.Minutes, 1);
				Assert.AreEqual(timecode.Seconds, 0);
			}

			[TestMethod]
			public void IsStable()
			{
				Timecode timecode = new Timecode(1, 2, 3);
				Assert.AreEqual(timecode.Seconds, 2);
			}
		}

		[TestClass]
		public class FramesProperty : TimecodeTests
		{
			[TestMethod]
			public void CarriesOver()
			{
				Timecode timecode = new Timecode(0, 0, Timecode.FramesPerSecond);
				Assert.AreEqual(timecode.Seconds, 1);
				Assert.AreEqual(timecode.Frames, 0);
			}

			[TestMethod]
			public void IsStable()
			{
				Timecode timecode = new Timecode(1, 2, 3);
				Assert.AreEqual(timecode.Frames, 3);
			}
		}

		[TestClass]
		public class EqualityMethod : TimecodeTests
		{
			[TestMethod]
			public void EqualValuesYieldTrue()
			{
				Timecode x = new Timecode(1);
				Timecode y = new Timecode(1);
				Assert.IsTrue(x == y);
			}

			[TestMethod]
			public void NonEqualValuesYieldFalse()
			{
				Timecode x = new Timecode(1);
				Timecode y = new Timecode(2);
				Assert.IsFalse(x == y);
			}
		}

		[TestClass]
		public class InequalityMethod : TimecodeTests
		{
			[TestMethod]
			public void EqualValuesYieldFalse()
			{
				Timecode x = new Timecode(1);
				Timecode y = new Timecode(1);
				Assert.IsFalse(x != y);
			}

			[TestMethod]
			public void NonEqualValuesYieldTrue()
			{
				Timecode x = new Timecode(1);
				Timecode y = new Timecode(2);
				Assert.IsTrue(x != y);
			}
		}

		[TestClass]
		public class ObjectEqualsMethod : TimecodeTests
		{
			[TestMethod]
			public void EqualValuesYieldTrue()
			{
				Timecode x = new Timecode(1);
				Timecode y = new Timecode(1);
				Assert.IsTrue(object.Equals(x, y));
				Assert.IsTrue(((object)x).Equals(y));
			}

			[TestMethod]
			public void NonEqualValuesYieldFalse()
			{
				Timecode x = new Timecode(1);
				Timecode y = new Timecode(2);
				Assert.IsFalse(object.Equals(x, y));
				Assert.IsFalse(((object)x).Equals(y));
			}

			[TestMethod]
			public void ZeroToOtherTypeYieldsFalse()
			{
				Assert.IsFalse(object.Equals(Timecode.Zero, "test"));
				Assert.IsFalse(((object)Timecode.Zero).Equals("test"));
			}

			[TestMethod]
			public void ZeroToNullYieldsFalse()
			{
				Assert.IsFalse(object.Equals(Timecode.Zero, null));
				Assert.IsFalse(((object)Timecode.Zero).Equals(null));
			}

			[TestMethod]
			public void NullToZeroYieldsFalse()
			{
				Assert.IsFalse(object.Equals(null, Timecode.Zero));
			}
		}

		[TestClass]
		public class IEquatableEqualsMethod : TimecodeTests
		{
			[TestMethod]
			public void EqualValuesYieldTrue()
			{
				Timecode x = new Timecode(1);
				Timecode y = new Timecode(1);
				Assert.IsTrue(x.Equals(y));
			}

			[TestMethod]
			public void NonEqualValuesYieldFalse()
			{
				Timecode x = new Timecode(1);
				Timecode y = new Timecode(2);
				Assert.IsFalse(x.Equals(y));
			}
		}

		[TestClass]
		public class IComparableCompareToMethod : TimecodeTests
		{
			Timecode small = Timecode.Zero;
			Timecode large = new Timecode(int.MaxValue);

			[TestMethod]
			public void LargerParameterYieldsNegative()
			{
				Assert.IsTrue(small.CompareTo(large) < 0);
			}

			[TestMethod]
			public void EqualValuesYieldZero()
			{
				Assert.AreEqual(large.CompareTo(large), 0);
			}

			[TestMethod]
			public void SmallerParameterYieldsPositive()
			{
				Assert.IsTrue(large.CompareTo(small) > 0);
			}
		}

		[TestClass]
		public class ComparisonOperators : TimecodeTests
		{
			[TestMethod]
			public void DifferentValuesTestCorrectly()
			{
				Timecode one = new Timecode(1);
				Timecode two = new Timecode(2);

				Assert.IsTrue(one < two);
				Assert.IsTrue(one <= two);
				Assert.IsFalse(one > two);
				Assert.IsFalse(one >= two);

				Assert.IsFalse(two < one);
				Assert.IsFalse(two <= one);
				Assert.IsTrue(two > one);
				Assert.IsTrue(two >= one);
			}

			[TestMethod]
			public void SameValuesTestCorrectly()
			{
				Timecode x = new Timecode(1);
				Timecode y = new Timecode(1);

				Assert.IsFalse(x < y);
				Assert.IsTrue(x <= y);
				Assert.IsFalse(x > y);
				Assert.IsTrue(x >= y);

				Assert.IsFalse(y < x);
				Assert.IsTrue(y <= x);
				Assert.IsFalse(y > x);
				Assert.IsTrue(y >= x);
			}
		}

		[TestClass]
		public class ObjectToStringMethod : TimecodeTests
		{
			[TestMethod]
			public void PartsAreZeroPaddedToTwoDigits()
			{
				Timecode x = new Timecode(1);
				Timecode y = new Timecode(1);
				Assert.AreEqual(Timecode.Zero.ToString(), "00:00:00");
			}

			[TestMethod]
			public void MinutesUseMoreDigitsAsNecesary()
			{
				Timecode timecode = new Timecode(123, 4, 5);
				Assert.AreEqual(timecode.ToString(), "123:04:05");
			}

			[TestMethod]
			public void RoundtripConversionYieldsEqualValue()
			{
				Timecode timecode = new Timecode(int.MaxValue);
				Assert.AreEqual(Timecode.Parse(timecode.ToString()), timecode);
			}
		}
	}
}
